﻿
using BoilenEditor.Properties;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using Shapes = System.Windows.Shapes;


namespace BoilenEditor.Primitives {

    public partial class TemplateFileData : DependencyObject {

        // <#@ assembly name="Boilen" #>
        private static readonly Regex IncludeAssembly = new Regex( @"<#@\s*assembly\s+name\s*=\s*""([^""]+)""\s*#>", RegexOptions.Compiled | RegexOptions.IgnoreCase );

        // Name prefix of the type generated by the TextTemplating engine.
        private static readonly Regex TextTemplatingStackTraceLine = new Regex( @"Microsoft\.VisualStudio\.TextTemplating[a-z0-9]{32}\.GeneratedTextTransformation\.", RegexOptions.Compiled );

        private string shadowDirectory_;

        public bool Save( bool forceTransform ) {
            try {
                bool success = SaveCore( forceTransform );
                return success;
            }
            finally {
                Ex.DeleteDirectory( this.shadowDirectory_, true );
            }
        }

        private bool SaveCore( bool forceTransform ) {
            if( !forceTransform && this.SourceFile.IsReferenceFile ) {
                bool saved = this.SourceFileContents.Save( );
                this.AddFilePathStatus( saved ? "Saved" : "Could not save", this.SourceFile.FilePath, false );
                return saved;
            }

            // TODO: make transformation asynchronous
            this.AddFilePathStatus( "Transforming", this.SourceFile.FilePath, true );

            Ex.DeleteDirectory( this.shadowDirectory_, false );
            Directory.CreateDirectory( this.shadowDirectory_ );
            string shadowSourcePath = Path.Combine( this.shadowDirectory_, this.SourceFile.Name );
            string shadowGeneratedPath = Path.Combine( this.shadowDirectory_, this.GeneratedFile.Name );
            var references = this.SourceFileContents.SaveShadow( shadowSourcePath );

            string parentName = Path.ChangeExtension( Path.GetFileNameWithoutExtension( this.SourceFile.FilePath ), ".cs" );
            string parentPath = Path.Combine( Path.GetDirectoryName( this.SourceFile.FilePath ), parentName );
            if( File.Exists( parentPath ) ) {
                string shadowParentPath = Path.Combine( this.shadowDirectory_, Path.GetFileName( parentPath ) );
                File.WriteAllBytes( shadowParentPath, File.ReadAllBytes( parentPath ) );
            }

            var project = (ProjectData)this.SourceFile.Parent;
            string binDirectory = project.GetBinDirectory( );
            this.AddFilePathStatus( "Referencing", binDirectory, true );

            string fileDirectory = Path.GetDirectoryName( this.SourceFile.FilePath );
            string fix = "Check that the file has been downloaded from source control.{0}{0}[{1}]".With( Environment.NewLine, this.SourceFile.FilePath );
            foreach( string reference in references ) {
                string referenceName = Path.GetFileName( reference );
                string referencePath = Path.GetFullPath( Path.Combine( fileDirectory, reference ) );
                string referenceDestination = Path.Combine( this.shadowDirectory_, referenceName );

                string failedFile = null;
                if( referenceName.OrdinalEqual( "Includes.tt" ) ) {
                    string[] lines;
                    if( !Ex.ReadAllLines( referencePath, fix, out lines ) )
                        return false;

                    bool success = true;
                    for( int i = 0; success && i < lines.Length; ++i ) {
                        string line = lines[i];
                        var match = IncludeAssembly.Match( line );
                        success = match.Success;

                        if( success ) {
                            string assembly = match.Groups[1].Captures[0].Value;
                            string assemblyName = assembly + ".dll";
                            string assemblyPath = Path.Combine( binDirectory, assemblyName );
                            string assemblyDestination = Path.Combine( this.shadowDirectory_, assemblyName );

                            bool copyResult = Ex.CopyFile( assemblyPath, assemblyDestination, "Try performing a Debug build of the project to ensure the referenced assembly has been copied to the bin directory." );
                            if( !copyResult )
                                failedFile = assemblyPath;

                            string docPath = Path.ChangeExtension( assemblyPath, ".xml" );
                            string docDestination = Path.ChangeExtension( assemblyDestination, ".xml" );
                            if( File.Exists( docPath ) ) {
                                copyResult = Ex.CopyFile( docPath, docDestination, "Could not copy assembly documentation file." );
                                if( !copyResult )
                                    failedFile = docPath;
                            }

                            lines[i] = line.Replace( assembly, assemblyDestination );
                        }
                    }

                    File.WriteAllLines( referenceDestination, lines );
                }
                else {
                    bool copyResult = Ex.CopyFile( referencePath, referenceDestination, fix );
                    if( !copyResult )
                        failedFile = referencePath;
                }

                if( !string.IsNullOrEmpty( failedFile ) ) {
                    this.AddFilePathStatus( "Could not copy", failedFile, false );
                    return false;
                }
            }

            ProcessStartInfo startInfo = new ProcessStartInfo {
                FileName = Settings.Default.TextTransformPath,
                Arguments = string.Format( "-out \"{0}\" \"{1}\"", shadowGeneratedPath, shadowSourcePath ),
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            };

            this.AddStatus( "Transform Arguments: " + startInfo.Arguments );

            Process process = Process.Start( startInfo );
            bool exited = process.WaitForExit( 10 * 1000 );

            if( exited && process.ExitCode == 0 ) {
                this.GeneratedFileContents.Content = Ex.ReadFile( shadowGeneratedPath );
                bool saved = this.GeneratedFileContents.Save( )
                          && this.SourceFileContents.Save( );

                if( saved )
                    this.AddFilePathStatus( "Saved", this.SourceFile.FilePath, false );
                else
                    this.AddFilePathStatus( "Could not save", this.SourceFile.FilePath, false );

                return saved;
            }
            else {
                if( !exited )
                    process.Kill( );

                string toolName = Path.GetFileName( Settings.Default.TextTransformPath );
                var output = new StringBuilder( );
                foreach( string line in process.StandardError.ReadLines( ) ) {
                    string error = TextTemplatingStackTraceLine.Replace( line, "" );
                    int parenIndex = line.IndexOf( '(' );
                    if( parenIndex > 0 ) {
                        int pathIndex = line.LastIndexOf( Path.DirectorySeparatorChar, parenIndex );
                        if( pathIndex > 0 )
                            error = line.Substring( pathIndex + 1 );
                    }

                    output.Append( Environment.NewLine ).Append( error );
                }

                string failureText = " {0} failed with exit code {1}: {2}".With( toolName, process.ExitCode, output );
                this.AddStatus( failureText, ( ) => new Span {
                    Inlines = {
                        new InlineUIContainer( new Shapes.Ellipse { Fill = Brushes.Red, Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center } ),
                        new Run( failureText )
                    }
                } );

                return false;
            }
        }


        private void UpdateFiles( ) {
            this.ClearStatus( );

            string shadowName = this.SourceFile.FilePath
                .Replace( this.SourceFile.RootDirectory, "" )
                .Replace( Path.DirectorySeparatorChar, '_' )
                .Replace( Path.AltDirectorySeparatorChar, '_' );
            this.shadowDirectory_ = Ex.GetShadowDirectory( shadowName );

            string sourcePath = this.SourceFile.FilePath;
            string sourceDirectory = Path.GetDirectoryName( sourcePath );
            string generatedFilePattern = Path.ChangeExtension( this.SourceFile.Name, ".*" );
            string generatedFilePath =
                Ex.GetFiles( sourceDirectory, generatedFilePattern, SearchOption.TopDirectoryOnly )
                    .OrderBy( path => Path.GetFileNameWithoutExtension( path ).Length )
                    .FirstOrDefault( path => !Path.GetExtension( path ).OrdinalEqual( ".tt" ) )
                    ?? Path.ChangeExtension( sourcePath, ".cs" );
            this.GeneratedFile = new FileData( generatedFilePath, this.SourceFile.RootDirectory, this.SourceFile );

            this.SourceFileContents = new FileContents( this.SourceFile );
            this.GeneratedFileContents = new FileContents( this.GeneratedFile );
            if( this.SourceFile.IsReferenceFile )
                this.GeneratedFileContents.Content = Environment.NewLine + "  // Note: This is a reference file. It can be saved, but will not be transformed.";

            this.AddFilePathStatus( "Opened", sourcePath, false );
            this.SourceFileContents.ContentChanged += this.ClearStatus;
        }

        private void AddFilePathStatus( string action, string path, bool progressing ) {
            string text = action + " " + path + (progressing ? "..." : "");
            this.AddStatus( text, ( ) => CreateFilePathStatus( action, path, progressing ) );
        }

        private void AddStatus( string text ) {
            this.AddStatus( text, ( ) => new Run( text ) );
        }

        private void AddStatus( string text, Func<Inline> createStatus ) {
            // Need unique UI elements for display in both status bar and tool tip.
            this.CurrentStatusText = text;
            this.CurrentStatus = createStatus( );
            this.Status.Add( createStatus( ) );
        }

        private void ClearStatus( ) {
            this.Status.Clear( );
            if( this.SourceFileContents.HasValue( ) )
                this.SourceFileContents.ContentChanged -= this.ClearStatus;
        }


        private void ClearStatus( object sender, EventArgs e ) {
            this.ClearStatus( );
        }

        private void OnSourceFileChanged( ) {
            this.UpdateFiles( );
        }


        private static Span CreateFilePathStatus( string action, string path, bool progressing ) {
            string directory = Path.GetDirectoryName( path ) + Path.DirectorySeparatorChar;
            string name = Path.GetFileName( path );

            Span status = new Span {
                Inlines = {
                    new Run( action + " " + directory ),
                    new Run( name ) { FontWeight = FontWeights.Bold }
                }
            };
            if( progressing )
                status.Inlines.Add( new Run( "..." ) );

            return status;
        }

    }

}
